package eztools

import (
	"encoding/csv"
	"encoding/xml"
	"io"
	"os"
	"path/filepath"

	"github.com/shibukawa/configdir"

	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
)

func xmlPrepare(data interface{}, indent string) ([]byte, error) {
	bytes, err := xml.MarshalIndent(data, "", indent)
	if err != nil {
		return nil, err
	}
	var PREFIX = []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
	return append(PREFIX, bytes...), nil
}

// XMLWrite writes file from input structure
// by a full path (with .xml extension)
// this reformats the file with indent provided
func XMLWrite(file string, data interface{}, indent string) error {
	slc, err := xmlPrepare(data, indent)
	if err != nil {
		return err
	}
	return FileWrite(file, slc)
}

// XMLWriter writes with writer from input structure
// wrt is not Close()'d
func XMLWriter(wrt io.WriteCloser, data interface{}, indent string) error {
	if wrt == nil {
		return ErrOutOfBound
	}
	slc, err := xmlPrepare(data, indent)
	if err != nil {
		return err
	}
	_, err = wrt.Write(slc)
	return err
}

// XMLWriteNoCreate writes existing file from input structure
// by a full path (with .xml extension)
// this reformats the file with indent provided
func XMLWriteNoCreate(file string, data interface{}, indent string) error {
	slc, err := xmlPrepare(data, indent)
	if err != nil {
		return err
	}
	return FileWriteNoCreate(file, slc)
}

// XMLWriteDefault writes file from input structure
// by a module name for both system config directory and file name
// home dir and current dir are also candidates in case of failure
// this reformats the file with indent provided
func XMLWriteDefault(module string, data interface{},
	indent string) (pathFound string, err error) {
	dirs := []string{GetCfgDir("", module)}
	home, err := os.UserHomeDir()
	if err == nil {
		dirs = append(dirs, home)
	}
	dirs = append(dirs, ".")
	var failPaths string
	for _, dir1 := range dirs {
		pathFound = filepath.Join(dir1, module+".xml")
		err = XMLWrite(pathFound, data, indent)
		Log("trying", pathFound, err)
		if err == nil {
			return
		}
		failPaths += "; " + pathFound
	}
	return failPaths, err
}

// XMLRead reads file into input structure
// Return values from os.Stat(), os.ReadFile() and xml.Unmarshal()
func XMLRead(file string, data interface{}) error {
	if _, err := os.Stat(file); err != nil {
		return err
	}
	bytes, err := os.ReadFile(file)
	if err != nil {
		return err
	}
	return xml.Unmarshal(bytes, data)
}

// XMLReader reads with reader into input structure
// rdr is not Close()'d
func XMLReader(rdr io.ReadCloser, data interface{}) error {
	bytes, err := io.ReadAll(rdr)
	if err != nil {
		return err
	}
	return xml.Unmarshal(bytes, data)
}

// GetCfgDir returns config directory by system
// Parameters: vendor can be empty
func GetCfgDir(vendor, module string) string {
	dirs := configdir.New(vendor, module)
	folders := dirs.QueryFolders(configdir.Global)
	//folders[0].WriteFile("cfg.xml", nil)
	return folders[0].Path
}

// XMLReadDefault reads config file into input structure from
//  1. givenpath,
//  2. or given file name (plus .xml) under one of these dirs,
//     current dir, home dir, or system's config dir.
//     the file name is taken as module name for system's config dir.
//
// Return values: full file name with path
//
//	ErrNoValidResults=no file parsable
//	other errors from XMLRead()
func XMLReadDefault(path, file string, cfg interface{}) (
	pathFound string, err error) {
	if len(path) > 0 {
		err = XMLRead(path, cfg)
		if err == nil {
			return path, err
		}
	}
	if len(file) < 1 {
		return "", ErrNoValidResults
	}
	home, _ := os.UserHomeDir()
	cfgPaths := [...]string{".", home, GetCfgDir("", file)}
	for _, path1 := range cfgPaths {
		pathFound = filepath.Join(path1, file+".xml")
		err = XMLRead(pathFound, cfg)
		if err == nil {
			if Debugging && Verbose > 1 {
				Log(pathFound, "found and read")
			}
			return
		} else {
			// TODO: return err for failure to access or parse
			if Debugging && Verbose > 1 {
				Log(pathFound, err)
			}
		}
	}
	return "", ErrNoValidResults
}

// CfgEZtools defines the structure of a predefined config xml
type CfgEZtools struct {
	// Root of the XML
	Root xml.Name `xml:"eztools"`
	// Cmt = comments
	Cmt string `xml:",comment"`
	// Text is not used
	Text string          `xml:",chardata"`
	Db   CfgEZtoolsDb    `xml:"db"`
	Map  []CfgEZtoolsMap `xml:"map"`
}

const (
	EncodingGbk = "gbk"
)

var encodings = [...]struct {
	str string
	enc encoding.Encoding
}{
	{EncodingGbk, simplifiedchinese.GBK},
}

// ListEnc list all valid encoding strings for
//
//		CSVReadWtEnc()
//	 UTF-8 is default and not listed.
func ListEnc() (ret []string) {
	for _, e := range encodings {
		ret = append(ret, e.str)
	}
	return ret
}

type file2Writer func(f *os.File, encodingStr string) *transform.Writer

func csvWriteWtWriter(file string, data [][]string, f2w file2Writer,
	encodingStr string, createIfNeeded bool) (err error) {
	_, err = os.Stat(file)
	var g int
	g = os.O_WRONLY | os.O_TRUNC
	if !createIfNeeded {
		if err != nil {
			return
		}
	} else {
		g |= os.O_CREATE
	}
	f, err := os.OpenFile(file, g, FileCreatePermission)
	if err != nil {
		return
	}
	defer f.Close()
	var wrt *csv.Writer
	if f2w != nil {
		wrt = csv.NewWriter(f2w(f, encodingStr))
	} else {
		wrt = csv.NewWriter(f)
	}
	return wrt.WriteAll(data)
}

// CSVWriteNoCreate writes data to existing file by a full path (with .csv extension)
func CSVWriteNoCreate(file string, data [][]string) error {
	return csvWriteWtWriter(file, data, nil, "", false)
}

// CSVWrite writes UTF-8 file with slice of slice of string
func CSVWrite(file string, data [][]string) (err error) {
	return csvWriteWtWriter(file, data, nil, "", true)
}

func csvWriterTransform(f *os.File, encodingStr string) *transform.Writer {
	var encING encoding.Encoding
	for _, v := range encodings {
		if v.str == encodingStr {
			encING = v.enc
			break
		}
	}
	return transform.NewWriter(f,
		encoding.ReplaceUnsupported(encING.NewEncoder()))
}

// CSVWriteWtEnc writes file with slice of slice of string, with specified encoding
func CSVWriteWtEnc(file, encodingStr string, data [][]string) (err error) {
	return csvWriteWtWriter(file, data,
		csvWriterTransform, encodingStr, true)
}

// CSVWriteWtEncNoCreate writes existing file with slice of slice of string, with specified encoding
func CSVWriteWtEncNoCreate(file, encodingStr string, data [][]string) (err error) {
	return csvWriteWtWriter(file, data,
		csvWriterTransform, encodingStr, false)
}

type file2Reader func(*os.File) *transform.Reader

func csvReadWtReader(file string, f2r file2Reader) (ret [][]string, err error) {
	if _, err = os.Stat(file); err != nil {
		return
	}
	f, err := os.Open(file)
	if err != nil {
		return
	}
	defer f.Close()
	var rdr *csv.Reader
	if f2r != nil {
		rdr = csv.NewReader(f2r(f))
	} else {
		rdr = csv.NewReader(f)
	}
	//rd := csv.NewReader(rdr)
	return rdr.ReadAll()
}

// CSVRead reads UTF-8 file into slice of slice of string
func CSVRead(file string) (ret [][]string, err error) {
	return csvReadWtReader(file, nil)
}

// CSVReadWtEnc reads file into slice of slice of string, with specified encoding
func CSVReadWtEnc(file, encodingStr string) (ret [][]string, err error) {
	return csvReadWtReader(file,
		func(f *os.File) *transform.Reader {
			var encING encoding.Encoding
			for _, v := range encodings {
				if v.str == encodingStr {
					encING = v.enc
					break
				}
			}
			return transform.NewReader(f,
				/*encoding.ReplaceUnsupported*/ (encING.NewDecoder()))
		})
}
